/*
 * Copyright (C) 2006 Moritz Tuerk <brotkasten@users.sourceforge.net>
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms 
 * of the GNU General Public License as published by the Free Software Foundation; either 
 * version 2 of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied 
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
 * See the GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License along with 
 *  this program; if not, write to the Free Software Foundation, 
 *  Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110, USA
 */
package mo.network.server.data;

import java.net.Socket;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Vector;

/**
 * Packets are used to communicate between a client and a server. They keep
 * information about the send status, send target as well as the actual payload
 * and some internally used flags. The only way a user (non client, non server)
 * should use the packets is:<br>
 * <ul>
 * <li>create them, assign a priority and payload and send them (for sending)</li>
 * <li>recieve them and get the payload (for recieving)</li>
 * </ul>
 * The priority of a packet <b>must not</b> be set to a value less than
 * {@link #PACKET_PRIORITY_MAX}. All higher priorities (lower prio values) are
 * used internally by the server and client.
 * 
 * @author brotkasten
 */
public class Packet implements Comparable<Packet> {
    /**
     * max resend count for fragments
     */
    private final static int FRAGMENT_MAX_RESEND = 3;

    /**
     * timeout for fragment sends
     */
    private static final int FRAGMENT_TIMEOUT = 3000;

    /**
     * this is the maximum priority for a packet to send. All priorities with a
     * numeric value below this are reserved for ACK and PING Packets and
     * <b>must not</b> used by applications! Applications may create relative
     * priorities like <code>PACKET_PRIORITY_MAX</code> or
     * <code>PACKET_PRIORITY_MAX+2</code>
     */
    public final static int PACKET_PRIORITY_MAX = 5;

    /**
     * this type is reserved for acknoledge messages! (value Integer.MAX_VALUE)
     */
    public final static int PACKET_TYPE_ACK = Integer.MAX_VALUE;

    /**
     * this type is reserved for packet fail messages! (value Integer.MAX_VALUE
     * - 1)
     */
    public final static int PACKET_TYPE_PF = Integer.MAX_VALUE - 1;

    /**
     * this type is reserved for packets with a ping message
     */
    public final static int PACKET_TYPE_PING = 1;

    /**
     * this type is reserved for packets with no set type! (value 0)
     */
    public final static int PACKET_TYPE_UNKNOWN = 0;

    /**
     * saves the socket
     */
    private final Socket clientSock;

    /**
     * saves a flag that indicates if a packet is completed (sent or recieved)
     */
    private boolean completed;

    /**
     * saves the current sent fragments waiting for an acknowledge
     */
    private Vector<Fragment> currentFragments;

    /**
     * saves the fragments of this packet
     */
    private Fragment[] fragments;

    /**
     * saves a set of fragments, that were sent already or recieved, depending
     * if this packet is an upload or download packet
     */
    private final HashSet<Fragment> fragmentSet;

    /**
     * used to lock resources
     */
    private final Object LOCK = new Object();

    /**
     * saves the stack number of this packet...
     */
    private long no;

    /**
     * saves a flag that indicates if a whole packet is timed out or not
     */
    private boolean packetFailed;

    /**
     * saves the packets priority (the less the value, the higher the prio)
     */
    private int priority;

    /**
     * saves the number of fragments processed
     */
    private int processedCount;

    /**
     * Creates a new packet to send
     * 
     * @param payLoad
     *            the payload of this packet. It is splitted in many fragments,
     *            if the payload is too big to be sent at once
     * @param clientSock
     *            saves the client socket to send the packet to
     */
    public Packet(final byte[] payLoad, final Socket clientSock) {
        this.fragmentSet = new HashSet<Fragment>();
        this.currentFragments = new Vector<Fragment>();

        this.packetFailed = false;
        this.completed = false;

        int fno = payLoad.length / Fragment.MAX_PAYLOAD;
        if (payLoad.length % Fragment.MAX_PAYLOAD != 0) {
            fno++;
        }

        this.fragments = new Fragment[fno];
        this.no = -1;
        this.priority = Packet.PACKET_PRIORITY_MAX;

        for (int i = 0; i < this.fragments.length; i++) {
            this.fragments[i] = new Fragment();
            this.fragments[i].count = fno;
            this.fragments[i].number = i;
            this.fragments[i].type = Packet.PACKET_TYPE_UNKNOWN;
            this.fragments[i].prio = this.priority;

            int size = payLoad.length - i * Fragment.MAX_PAYLOAD;
            if (size > Fragment.MAX_PAYLOAD) {
                size = Fragment.MAX_PAYLOAD;
            }

            final byte buffer[] = new byte[size];
            System
                    .arraycopy(payLoad, i * Fragment.MAX_PAYLOAD, buffer, 0,
                            size);

            this.fragments[i].size = size;
            this.fragments[i].payload = buffer;
        }

        this.clientSock = clientSock;
    }

    /**
     * Creates a new Packet to recieve.
     * 
     * @param clientSock
     *            saves the client (socket)
     */
    public Packet(final Socket clientSock) {
        this.clientSock = clientSock;

        this.fragmentSet = new HashSet<Fragment>();
        this.packetFailed = false;
        this.completed = false;

        this.no = -1;
        this.processedCount = 0;
        this.priority = Packet.PACKET_PRIORITY_MAX;
    }

    /**
     * Call this method to add a new fragment to this packet (if this is a
     * packet to be recieved by a client)
     * 
     * @param next
     *            the next fragment recieved by a client.
     */
    public void addFragment(final Fragment next) {
        if (this.fragments == null) {
            this.fragments = new Fragment[next.count];

            // set the packet id!
            this.no = next.packetId;

            // set the packet prio!
            this.priority = next.prio;

            this.processedCount = 0;
        }

        if (this.fragments[next.number] != null) {
            this.fragments[next.number].lastSendTime = System
                    .currentTimeMillis();
            this.fragments[next.number].sendCount++;

        } else {
            this.fragments[next.number] = next;
            next.lastSendTime = System.currentTimeMillis();
            next.sendCount = 1;

            this.processedCount++;

            this.completed = this.fragments.length == this.processedCount;
        }
    }

    /**
     * Is used to compare packets. They are compared by their priority.
     * 
     * @param other
     *            saves the other object to compare to (only makes sense with
     *            other packets)
     * @return -1 if the prio of this packet is higher than the given one, +1 if
     *         the prio is less, 0 if the priorities are equal to each other
     */
    public int compareTo(final Packet other) {
        int result = this.priority - other.priority;
        if (result == 0) {
            result = (int) (this.getNo() - other.getNo());
        }
        return result;
    }

    /**
     * returns the number of fragments in this packet
     * 
     * @return the number of fragments in this packet
     */
    public int getFragmentCount() {
        if (this.fragments == null) {
            return 0;
        }

        return this.fragments.length;
    }

    /**
     * This method returns the next fragment to send.
     * 
     * @param update
     *            set this to <code>true</code> if you REALLY send this
     *            fragment. If you just like to check it, set this to
     *            <code>false</code>.
     * @param now
     *            saves the time which is intepreted as 'now'
     * @return the next fragment of this packet to send
     */
    public Fragment getNext2Send(final boolean update, final long now) {
        Fragment toSend = null;

        // synchronized: currentFragments, fragmentSet
        synchronized (this.LOCK) {
            final Iterator<Fragment> fiter = this.currentFragments.iterator();
            while (toSend == null && fiter.hasNext()) {
                final Fragment next = fiter.next();

                // need to be resend ?
                if (now - next.lastSendTime > Packet.FRAGMENT_TIMEOUT) {
                    if (update) {
                        next.lastSendTime = System.currentTimeMillis();
                        next.sendCount++;
                    }
                    if (next.sendCount > Packet.FRAGMENT_MAX_RESEND) {
                        if (update) {
                            this.packetFailed = true;
                        }
                    } else {
                        toSend = next;
                    }
                }
            }

            if (!this.packetFailed && this.currentFragments.size() < 256) {
                for (int i = 0; toSend == null && i < this.fragments.length; i++) {
                    if (!this.fragmentSet.contains(this.fragments[i])
                            && !this.currentFragments
                                    .contains(this.fragments[i])) {
                        toSend = this.fragments[i];

                        if (update) {
                            toSend.lastSendTime = System.currentTimeMillis();
                            toSend.sendCount = 1;

                            this.currentFragments.add(toSend);
                        }
                    }
                }
            }
        }

        return toSend;
    }

    /**
     * Call this method to get the stack number of this packet. An unique stack
     * number is assigned to each packet. This is a way to identify a packet on
     * server side.
     * 
     * @return the stack number of this packet.
     */
    public long getNo() {
        return this.no;
    }

    /**
     * Returns the payload of this packet. The payloads of all the fragments are
     * concatenated and returned.
     * 
     * @return the payload of this packet
     */
    public byte[] getPayLoad() {
        synchronized (this.LOCK) {
            if (this.fragments == null) {
                return null;
            }
        }

        final byte result[] = new byte[(this.fragments.length - 1)
                * Fragment.MAX_PAYLOAD
                + this.fragments[this.fragments.length - 1].size];
        for (int i = 0; i < this.fragments.length; i++) {
            System.arraycopy(this.fragments[i].payload, 0, result, i
                    * Fragment.MAX_PAYLOAD, this.fragments[i].size);
        }

        return result;
    }

    /**
     * Call this method to get the priority of this packet.
     * 
     * @return the priority of this packet. The less the value the higher the
     *         priority
     */
    public int getPriority() {
        return this.priority;
    }

    /**
     * call this method to get the client socket assigned to this packet. The
     * client socket is either the sender or reciever socket, depending on
     * whether the packet is sent or recieved
     * 
     * @return the client socket of this packet
     */
    public Socket getSocket() {
        return this.clientSock;
    }

    /**
     * Returns the type of this packet. The type of a packet is user defined
     * (except of the two values {@link #PACKET_TYPE_ACK} and
     * {@link #PACKET_TYPE_UNKNOWN})
     * 
     * @return the packet type
     */
    public int getType() {
        for (final Fragment element : this.fragments) {
            if (element != null) {
                return element.type;
            }

        }
        return Packet.PACKET_TYPE_UNKNOWN;
    }

    /**
     * This method can be called to check if a packet is sent successfully or
     * recieved completely.
     * 
     * @return <code>true</code> if this packet is complete, else
     *         <code>false</code>
     */
    public boolean isComplete() {
        return this.completed;
    }

    /**
     * This method can be called to check if a packet is timed out during a send
     * or recieve operation. A timeout happens, when a fragment was sent at
     * least {@link #FRAGMENT_MAX_RESEND} times and wasn't acknowleged during
     * the period of {@link #FRAGMENT_TIMEOUT} milliseconds.
     * 
     * @return <code>true</code> if the packet failed to be sent or recieved,
     *         else <code>false</code>
     */
    public boolean isPacketFailed() {
        return this.packetFailed;
    }

    /**
     * Call this method to mark a fragment as sent (and acknowledged)
     * 
     * @param fragmentNo
     *            the fragment number of the fragment that was sent
     */
    public void markAsSent(final int fragmentNo) {
        synchronized (this.LOCK) {
            final Fragment f = this.fragments[fragmentNo];

            this.currentFragments.remove(f);
            this.fragmentSet.add(f);

            this.completed = this.fragmentSet.size() == this.fragments.length;
        }
    }

    /**
     * This method is called to set the stack number of this packet. DO NOT SET
     * THIS MANUALLY, it is overriden by the network server.
     * 
     * @param no
     *            saves the stack number of this packet, this is used as ID in a
     *            network session. Each packet send by this server (except of
     *            ACK packets) gets a unique stack number. This is very much
     *            like an TCP Stack. This stack no is also published to the
     *            fragments of this packet.
     */
    public void setNo(final long no) {
        this.no = no;

        if (this.fragments != null) {
            for (int i = 0; i < this.fragments.length; i++) {
                if (this.fragments[i] != null) {
                    this.fragments[i].packetId = no;
                }
            }
        }
    }

    /**
     * Use this method to set the priority for this packet
     * 
     * @param prio
     *            saves the priority for this packet
     */
    public void setPriority(final int prio) {
        this.priority = prio;
        if (this.fragments != null) {
            for (int i = 0; i < this.fragments.length; i++) {
                if (this.fragments[i] != null) {
                    this.fragments[i].prio = prio;
                }
            }
        }
    }

    /**
     * @param type
     */
    public void setType(final int type) {
        if (this.fragments != null) {
            for (int i = 0; i < this.fragments.length; i++) {
                if (this.fragments[i] != null) {
                    this.fragments[i].type = type;
                }
            }
        }
    }

    @Override
    public String toString() {
        final StringBuffer result = new StringBuffer(
                "\n----------------------[PACKET]-----------------------\n");
        result.append("no : " + this.no + "\n");
        result.append("pl : " + new String(this.getPayLoad()) + "\n");
        result
                .append("----------------------[PACKET]-----------------------\n");
        return result.toString();
    }
}
